File: LanguageService\AbstractLanguageService`2.VsLanguageDebugInfo.cs
Web Access
Project: src\src\VisualStudio\Core\Def\Microsoft.VisualStudio.LanguageServices_e5lazejx_wpftmp.csproj (Microsoft.VisualStudio.LanguageServices)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Debugging;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Text.Shared.Extensions;
using Microsoft.VisualStudio.LanguageServices.Implementation.Extensions;
using Microsoft.VisualStudio.LanguageServices.Implementation.Utilities;
using Microsoft.VisualStudio.Utilities;
using Roslyn.Utilities;
using IVsDebugName = Microsoft.VisualStudio.TextManager.Interop.IVsDebugName;
using IVsEnumBSTR = Microsoft.VisualStudio.TextManager.Interop.IVsEnumBSTR;
using IVsTextBuffer = Microsoft.VisualStudio.TextManager.Interop.IVsTextBuffer;
using RESOLVENAMEFLAGS = Microsoft.VisualStudio.TextManager.Interop.RESOLVENAMEFLAGS;
using VsTextSpan = Microsoft.VisualStudio.TextManager.Interop.TextSpan;
 
namespace Microsoft.VisualStudio.LanguageServices.Implementation.LanguageService;
 
internal abstract partial class AbstractLanguageService<TPackage, TLanguageService>
{
    internal sealed class VsLanguageDebugInfo : IVsLanguageDebugInfo
    {
        private readonly Guid _languageId;
        private readonly TLanguageService _languageService;
        private readonly IThreadingContext _threadingContext;
        private readonly ILanguageDebugInfoService? _languageDebugInfo;
        private readonly IBreakpointResolutionService? _breakpointService;
        private readonly IProximityExpressionsService? _proximityExpressionsService;
        private readonly IUIThreadOperationExecutor _uiThreadOperationExecutor;
 
        public VsLanguageDebugInfo(
            Guid languageId,
            TLanguageService languageService,
            HostLanguageServices languageServiceProvider,
            IThreadingContext threadingContext,
            IUIThreadOperationExecutor uiThreadOperationExecutor)
        {
            Contract.ThrowIfNull(languageService);
            Contract.ThrowIfNull(languageServiceProvider);
 
            _languageId = languageId;
            _languageService = languageService;
            _threadingContext = threadingContext;
            _languageDebugInfo = languageServiceProvider.GetService<ILanguageDebugInfoService>();
            _breakpointService = languageServiceProvider.GetService<IBreakpointResolutionService>();
            _proximityExpressionsService = languageServiceProvider.GetService<IProximityExpressionsService>();
            _uiThreadOperationExecutor = uiThreadOperationExecutor;
        }
 
        public int GetLanguageID(IVsTextBuffer pBuffer, int iLine, int iCol, out Guid pguidLanguageID)
        {
            pguidLanguageID = _languageId;
            return VSConstants.S_OK;
        }
 
        public int GetLocationOfName(string pszName, out string? pbstrMkDoc, out VsTextSpan pspanLocation)
        {
            pbstrMkDoc = null;
            pspanLocation = default;
            return VSConstants.E_NOTIMPL;
        }
 
        public int GetNameOfLocation(IVsTextBuffer pBuffer, int iLine, int iCol, out string? pbstrName, out int piLineOffset)
        {
            using (Logger.LogBlock(FunctionId.Debugging_VsLanguageDebugInfo_GetNameOfLocation, CancellationToken.None))
            {
                string? name = null;
                var lineOffset = 0;
 
                if (_languageDebugInfo != null)
                {
                    _uiThreadOperationExecutor.Execute(
                        title: ServicesVSResources.Debugger,
                        defaultDescription: ServicesVSResources.Determining_breakpoint_location,
                        allowCancellation: true,
                        showProgress: false,
                        action: waitContext =>
                        {
                            var cancellationToken = waitContext.UserCancellationToken;
                            var textBuffer = _languageService.EditorAdaptersFactoryService.GetDataBuffer(pBuffer);
                            if (textBuffer != null)
                            {
                                var nullablePoint = textBuffer.CurrentSnapshot.TryGetPoint(iLine, iCol);
                                if (nullablePoint.HasValue)
                                {
                                    var point = nullablePoint.Value;
                                    var document = point.Snapshot.GetOpenDocumentInCurrentContextWithChanges();
 
                                    if (document != null)
                                    {
                                        // NOTE(cyrusn): We have to wait here because the debuggers' 
                                        // GetNameOfLocation is a blocking call.  In the future, it 
                                        // would be nice if they could make it async.
                                        _threadingContext.JoinableTaskFactory.Run(async () =>
                                        {
                                            var debugLocationInfo = await _languageDebugInfo.GetLocationInfoAsync(document, point, cancellationToken).ConfigureAwait(false);
 
                                            if (!debugLocationInfo.IsDefault)
                                            {
                                                name = debugLocationInfo.Name;
                                                lineOffset = debugLocationInfo.LineOffset;
                                            }
                                        });
                                    }
                                }
                            }
                        });
 
                    if (name != null)
                    {
                        pbstrName = name;
                        piLineOffset = lineOffset;
                        return VSConstants.S_OK;
                    }
                }
 
                // Note(DustinCa): Docs say that GetNameOfLocation should return S_FALSE if a name could not be found.
                // Also, that's what the old native code does, so we should do it here.
                pbstrName = null;
                piLineOffset = 0;
                return VSConstants.S_FALSE;
            }
        }
 
        public int GetProximityExpressions(IVsTextBuffer pBuffer, int iLine, int iCol, int cLines, out IVsEnumBSTR? ppEnum)
        {
            // NOTE(cyrusn): cLines is ignored.  This is to match existing dev10 behavior.
            using (Logger.LogBlock(FunctionId.Debugging_VsLanguageDebugInfo_GetProximityExpressions, CancellationToken.None))
            {
                VsEnumBSTR? enumBSTR = null;
 
                if (_proximityExpressionsService != null)
                {
                    _uiThreadOperationExecutor.Execute(
                        title: ServicesVSResources.Debugger,
                        defaultDescription: ServicesVSResources.Determining_autos,
                        allowCancellation: true,
                        showProgress: false,
                        action: context =>
                    {
                        var textBuffer = _languageService.EditorAdaptersFactoryService.GetDataBuffer(pBuffer);
 
                        if (textBuffer != null)
                        {
                            var snapshot = textBuffer.CurrentSnapshot;
                            var nullablePoint = snapshot.TryGetPoint(iLine, iCol);
                            if (nullablePoint.HasValue)
                            {
                                var document = snapshot.GetOpenDocumentInCurrentContextWithChanges();
                                if (document != null)
                                {
                                    var point = nullablePoint.Value;
                                    var proximityExpressions = _proximityExpressionsService.GetProximityExpressionsAsync(document, point.Position, context.UserCancellationToken).WaitAndGetResult(context.UserCancellationToken);
 
                                    if (proximityExpressions != null)
                                    {
                                        enumBSTR = new VsEnumBSTR(proximityExpressions);
                                    }
                                }
                            }
                        }
                    });
                }
 
                ppEnum = enumBSTR;
                return ppEnum != null ? VSConstants.S_OK : VSConstants.E_FAIL;
            }
        }
 
        public int IsMappedLocation(IVsTextBuffer pBuffer, int iLine, int iCol)
            => VSConstants.E_NOTIMPL;
 
        public int ResolveName(string pszName, uint dwFlags, out IVsEnumDebugName? ppNames)
        {
            using (Logger.LogBlock(FunctionId.Debugging_VsLanguageDebugInfo_ResolveName, CancellationToken.None))
            {
                // In VS, this method frequently get's called with an empty string to test if the language service
                // supports this method (some language services, like F#, implement IVsLanguageDebugInfo but don't
                // implement this method).  In that scenario, there's no sense doing work, so we'll just return
                // S_FALSE (as the old VB language service did).
                if (string.IsNullOrEmpty(pszName))
                {
                    ppNames = null;
                    return VSConstants.S_FALSE;
                }
 
                VsEnumDebugName? enumName = null;
                _uiThreadOperationExecutor.Execute(
                    title: ServicesVSResources.Debugger,
                    defaultDescription: ServicesVSResources.Resolving_breakpoint_location,
                    allowCancellation: true,
                    showProgress: false,
                    action: waitContext =>
                {
                    _threadingContext.JoinableTaskFactory.Run(async () =>
                    {
                        var cancellationToken = waitContext.UserCancellationToken;
                        if (dwFlags == (uint)RESOLVENAMEFLAGS.RNF_BREAKPOINT)
                        {
                            var solution = _languageService.Workspace.CurrentSolution;
 
                            // NOTE(cyrusn): We have to wait here because the debuggers' ResolveName
                            // call is synchronous.  In the future it would be nice to make it async.
                            if (_breakpointService != null)
                            {
                                var breakpoints = await _breakpointService.ResolveBreakpointsAsync(
                                    solution, pszName, cancellationToken).ConfigureAwait(false);
                                var debugNames = await breakpoints.SelectAsArrayAsync(
                                    bp => CreateDebugNameAsync(bp, cancellationToken)).ConfigureAwait(false);
 
                                enumName = new VsEnumDebugName(debugNames);
                            }
                        }
                    });
                });
 
                ppNames = enumName;
                return ppNames != null ? VSConstants.S_OK : VSConstants.E_NOTIMPL;
            }
        }
 
        private async ValueTask<IVsDebugName> CreateDebugNameAsync(
            BreakpointResolutionResult breakpoint, CancellationToken cancellationToken)
        {
            var document = breakpoint.Document;
            var filePath = _languageService.Workspace.GetFilePath(document.Id);
            var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
            var span = text.GetVsTextSpanForSpan(breakpoint.TextSpan);
            // If we're inside an Venus code nugget, we need to map the span to the surface buffer.
            // Otherwise, we'll just use the original span.
            var mappedSpan = await span.MapSpanFromSecondaryBufferToPrimaryBufferAsync(
                _threadingContext, document.Id, cancellationToken).ConfigureAwait(false);
            if (mappedSpan != null)
                span = mappedSpan.Value;
 
            return new VsDebugName(breakpoint.LocationNameOpt, filePath, span);
        }
 
        public int ValidateBreakpointLocation(IVsTextBuffer pBuffer, int iLine, int iCol, VsTextSpan[] pCodeSpan)
        {
            using (Logger.LogBlock(FunctionId.Debugging_VsLanguageDebugInfo_ValidateBreakpointLocation, CancellationToken.None))
            {
                var result = VSConstants.E_NOTIMPL;
                _uiThreadOperationExecutor.Execute(
                    title: ServicesVSResources.Debugger,
                    defaultDescription: ServicesVSResources.Validating_breakpoint_location,
                    allowCancellation: true,
                    showProgress: false,
                    action: waitContext =>
                {
                    result = ValidateBreakpointLocationWorker(pBuffer, iLine, iCol, pCodeSpan, waitContext.UserCancellationToken);
                });
 
                return result;
            }
        }
 
        private int ValidateBreakpointLocationWorker(
            IVsTextBuffer pBuffer,
            int iLine,
            int iCol,
            VsTextSpan[] pCodeSpan,
            CancellationToken cancellationToken)
        {
            if (_breakpointService == null)
            {
                return VSConstants.E_FAIL;
            }
 
            var textBuffer = _languageService.EditorAdaptersFactoryService.GetDataBuffer(pBuffer);
            if (textBuffer != null)
            {
                var snapshot = textBuffer.CurrentSnapshot;
                var nullablePoint = snapshot.TryGetPoint(iLine, iCol);
                if (nullablePoint == null)
                {
                    // The point disappeared between sessions. Do not allow a breakpoint here.
                    return VSConstants.E_FAIL;
                }
 
                var document = snapshot.AsText().GetDocumentWithFrozenPartialSemantics(cancellationToken);
                if (document != null)
                {
                    var point = nullablePoint.Value;
                    var length = 0;
                    if (pCodeSpan != null && pCodeSpan.Length > 0)
                    {
                        // If we have a non-empty span then it means that the debugger is asking us to adjust an
                        // existing span.  In Everett we didn't do this so we had some good and some bad
                        // behavior.  For example if you had a breakpoint on: "int i = 1;" and you changed it to "int
                        // i = 1, j = 2;", then the breakpoint wouldn't adjust.  That was bad.  However, if you had the
                        // breakpoint on an open or close curly brace then it would always "stick" to that brace
                        // which was good.
                        //
                        // So we want to keep the best parts of both systems.  We want to appropriately "stick"
                        // to tokens and we also want to adjust spans intelligently.
                        //
                        // However, it turns out the latter is hard to do when there are parse errors in the
                        // code.  Things like missing name nodes cause a lot of havoc and make it difficult to
                        // track a closing curly brace.
                        //
                        // So the way we do this is that we default to not intelligently adjusting the spans
                        // while there are parse errors.  But when there are no parse errors then the span is
                        // adjusted.
                        var initialBreakpointSpan = snapshot.GetSpan(pCodeSpan[0]);
                        if (initialBreakpointSpan.Length > 0 && document.SupportsSyntaxTree)
                        {
                            var tree = document.GetSyntaxTreeSynchronously(cancellationToken);
                            Contract.ThrowIfNull(tree);
                            if (tree.GetDiagnostics(cancellationToken).Any(d => d.Severity == DiagnosticSeverity.Error))
                            {
                                // Keep the span as is.
                                return VSConstants.S_OK;
                            }
                        }
 
                        // If a span is provided, and the requested position falls in that span, then just
                        // move the requested position to the start of the span.
                        // Length will be used to determine if we need further analysis, which is only required when text spans multiple lines.
                        if (initialBreakpointSpan.Contains(point))
                        {
                            point = initialBreakpointSpan.Start;
                            length = pCodeSpan[0].iEndLine > pCodeSpan[0].iStartLine ? initialBreakpointSpan.Length : 0;
                        }
                    }
 
                    // NOTE(cyrusn): we need to wait here because ValidateBreakpointLocation is
                    // synchronous.  In the future, it would be nice for the debugger to provide
                    // an async entry point for this.
                    var breakpoint = _breakpointService.ResolveBreakpointAsync(document, new TextSpan(point.Position, length), cancellationToken).WaitAndGetResult(cancellationToken);
                    if (breakpoint == null)
                    {
                        // There should *not* be a breakpoint here.  E_FAIL to let the debugger know
                        // that.
                        return VSConstants.E_FAIL;
                    }
 
                    if (breakpoint.IsLineBreakpoint)
                    {
                        // Let the debugger take care of this. They'll put a line breakpoint
                        // here. This is useful for when the user does something like put a
                        // breakpoint in inactive code.  We want to allow this as they might
                        // just have different defines during editing versus debugging.
 
                        // TODO(cyrusn): Do we need to set the pCodeSpan in this case?
                        return VSConstants.E_NOTIMPL;
                    }
 
                    // There should be a breakpoint at the location passed back.
                    if (pCodeSpan != null && pCodeSpan.Length > 0)
                    {
                        pCodeSpan[0] = breakpoint.TextSpan.ToSnapshotSpan(snapshot).ToVsTextSpan();
                    }
 
                    return VSConstants.S_OK;
                }
            }
 
            return VSConstants.E_NOTIMPL;
        }
    }
}